-module(runner).
-export([test/0, run/1, run/2, evaluate/2]).
%-compile([native]).
-include("../include/environment.hrl").

calc_sum({T1, Val1}, {T2, Val2}) -> {vars:common_numeric(T1, T2), Val1+Val2}.
calc_diff({T1, Val1}, {T2, Val2}) -> {vars:common_numeric(T1, T2), Val1-Val2}.
calc_produkt({T1, Val1}, {T2, Val2}) -> {vars:common_numeric(T1, T2), Val1*Val2}.
calc_quoshunt({T1, Val1}, {T2, Val2}) -> {vars:common_numeric(T1, T2), Val1/Val2}.
calc_mod({T1, Val1}, {T2, Val2}) -> {vars:common_numeric(T1, T2), round(Val1) rem round(Val2)}.

calc_both({troof, win}, {troof, win}) -> {troof, win};
calc_both({troof, _}, {troof, _}) -> {troof, fail}.

calc_either({troof, fail}, {troof, fail}) -> {troof, fail};
calc_either({troof, _}, {troof, _}) -> {troof, win}.

calc_one({troof, fail}, {troof, win}) -> {troof, win};
calc_one({troof, win}, {troof, fail}) -> {troof, win};
calc_one({troof, _}, {troof, _}) -> {troof, fail}.

calc_same({numbr, Arg}, {numbr, Arg}) -> {troof, win};
calc_same({numbar, Arg}, {numbar, Arg}) -> {troof, win};
calc_same({yarn, Arg}, {yarn, Arg}) -> {troof, win};
calc_same({noob, noob}, {noob, noob}) -> {troof, win};
calc_same({numbr, _}=Arg1, {numbar, _}=Arg2) -> calc_same(vars:cast(Arg1, {type, numbar}), Arg2);
calc_same({numbar, _}=Arg1, {numbr, _}=Arg2) -> calc_same(Arg1, vars:cast(Arg2, {type, numbar}));
calc_same({_, _}, {_, _}) -> {troof, fail}.

calc_diffrent(Arg1, Arg2) -> calc_not(calc_same(Arg1, Arg2)).

calc_biggr({_, Val1}=Arg1, {_, Val2}=Arg2) ->
    if
        Val1>Val2 -> Arg1;
        true -> Arg2
    end.
calc_smallr({_, Val1}=Arg1, {_, Val2}=Arg2) ->
    if
        Val1<Val2 -> Arg1;
        true -> Arg2
    end.

calc_not({troof, fail}) -> {troof, win};
calc_not({troof, win}) -> {troof, fail}.

calc_binary_math(kw_sum, Arg1, Arg2) -> calc_sum(Arg1, Arg2);
calc_binary_math(kw_diff, Arg1, Arg2) -> calc_diff(Arg1, Arg2);
calc_binary_math(kw_produkt, Arg1, Arg2) -> calc_produkt(Arg1, Arg2);
calc_binary_math(kw_quoshunt, Arg1, Arg2) -> calc_quoshunt(Arg1, Arg2);
calc_binary_math(kw_mod, Arg1, Arg2) -> calc_mod(Arg1, Arg2);
calc_binary_math(kw_biggr, Arg1, Arg2) -> calc_biggr(Arg1, Arg2);
calc_binary_math(kw_smallr, Arg1, Arg2) -> calc_smallr(Arg1, Arg2).

calc_binary_logic(kw_both, Arg1, Arg2) -> calc_both(Arg1, Arg2);
calc_binary_logic(kw_won, Arg1, Arg2) -> calc_one(Arg1, Arg2);
calc_binary_logic(kw_either, Arg1, Arg2) -> calc_either(Arg1, Arg2).

calc_binary_compare(kw_both, Arg1, Arg2) -> calc_same(Arg1, Arg2);
calc_binary_compare(kw_diffrint, Arg1, Arg2) -> calc_diffrent(Arg1, Arg2).

evaluate(_, {yarn, _}=Res) -> Res;
evaluate(_, {numbr, _}=Res) -> Res;
evaluate(_, {numbar, _}=Res) -> Res;
evaluate(_, {troof, _}=Res) -> Res;
evaluate(_, {noob, _}=Res) -> Res;
evaluate(Environment, {identifier, Name}) -> environment:get_var(Environment, Name);
evaluate(Environment, {cast, Exp, {type,_}=Type}) ->
    Val = evaluate(Environment, Exp),
    vars:cast(Val, Type);
evaluate(Environment, {unary_op, kw_not, Arg}) ->
    Earg = vars:cast(evaluate(Environment, Arg), {type, troof}),
    calc_not(Earg);
evaluate(Environment, {binary_math_op, Op, Arg1, Arg2}) ->
    Earg1 = vars:conv_to_numeric(evaluate(Environment, Arg1)),
    Earg2 = vars:conv_to_numeric(evaluate(Environment, Arg2)),
    calc_binary_math(Op, Earg1, Earg2);
evaluate(Environment, {binary_compare_op, Op, Arg1, Arg2}) ->
    Earg1 = evaluate(Environment, Arg1),
    Earg2 = evaluate(Environment, Arg2),
    calc_binary_compare(Op, Earg1, Earg2);
evaluate(Environment, {binary_logic_op, Op, Arg1, Arg2}) ->
    Earg1 = vars:cast(evaluate(Environment, Arg1), {type, troof}),
    Earg2 = vars:cast(evaluate(Environment, Arg2), {type, troof}),
    calc_binary_logic(Op, Earg1, Earg2);
evaluate(Environment, {bukkit_selector, Bukkit, Key}) ->
    vars:bucket_get(evaluate(Environment, Bukkit),
        lists:map(fun(A) -> evaluate(Environment, A) end, Key)).

cast_inplace_block(_Browsed, [], _Var, _Type) -> not_found;
cast_inplace_block(Browsed, [{Name, Val}|Other], Var, Type) when is_list(Browsed),Name == Var ->
    Browsed ++ [{Name, vars:cast(Val, Type)}] ++ Other;
cast_inplace_block(Browsed, [Current|Other], Var, Type) when is_list(Browsed) ->
    cast_inplace_block(Browsed++[Current], Other, Var, Type).

cast_inplace_multiblock(Browsed, [Current|Other], Var, Type) when is_list(Browsed) ->
    case cast_inplace_block([], Current, Var, Type) of
        NewCurrent when is_list(NewCurrent) -> Browsed ++ [NewCurrent] ++ Other;
        not_found -> cast_inplace_multiblock(Browsed ++ [Current], Other, Var, Type)
    end.
cast_inplace(Vars, Var, Type) -> cast_inplace_multiblock([], Vars, Var, Type).

buildin_visible(Environment, []) -> Environment;
buildin_visible(Environment, [Term|Terms]) ->
    {yarn, Yarn} = vars:cast(evaluate(Environment, Term), {type, yarn}),
    NewEnv = environment:push_output(Environment, Yarn),
    buildin_visible(NewEnv, Terms).

cut_newline(Processed, [$\n]) -> Processed;
cut_newline(Processed, []) -> Processed;
cut_newline(Processed, [X|Xs]) -> cut_newline(Processed++[X], Xs).

buildin_gimmeh(Environment) ->
    case environment:get_input(Environment) of
        eof -> {noob, noob};
        Line when is_list(Line) -> 
			{yarn, cut_newline([], Line)}
    end.

try_mebbes(_Environment, []) -> not_found;
try_mebbes(Environment, [{Cond, Program}|Mebbes]) ->
    case vars:cast(evaluate(Environment, Cond), {type, troof}) of
        {troof, win} -> {ok, engine(environment:new_scope(Environment), Program, false)};
        {troof, fail} -> try_mebbes(Environment, Mebbes)
    end.

try_cases(Environment, []) -> Environment;
try_cases(Environment, [{catchall,Code}]) ->
    engine(environment:new_scope(Environment), Code, false);
try_cases(Environment, [{Cond,Code}|Cases]) ->
    case calc_same(environment:get_var(Environment, "IT"), evaluate(Environment,Cond)) of
        {troof, win} ->
            Program = lists:foldl(fun({_,X},S) -> S++X end, Code, Cases),
            engine(environment:new_scope(Environment), Program, false);
        {troof, fail} -> try_cases(Environment, Cases)
    end.

engine(Environment, [], _Looped) ->
    environment:strip_scope(Environment);
engine(Environment, [Instruction|Program], Looped) ->
    NewEnv = case Instruction of
        {visible, Terms} when is_list(Terms) ->
            buildin_visible(Environment, Terms);
        {gimmeh, {identifier, Var}} ->
            ({_,_}=Value)=buildin_gimmeh(Environment),
            environment:create_var(Environment, Var, Value);
        {gimmeh, {bukkit_selector, _, _}=Bukkit} ->
            ({_,_}=Value)=buildin_gimmeh(Environment),
            environment:assign_var(Environment, Bukkit, Value);
        {assignment, Var, Exp} ->
            environment:assign_var(Environment, Var, evaluate(Environment, Exp));
        {assignment_new, Var, Exp} ->
            environment:create_var(Environment, Var, evaluate(Environment, Exp));
        {cast_inplace, {identifier, Var}, {type,_}=Type} ->
            environment:assign_var(Environment, Var, vars:cast(environment:get_var(Var), Type));
        {cast, Exp, {type,_}=Type} ->
            environment:assign_var(Environment, {identifier, "IT"}, vars:cast(evaluate(Environment, Exp), Type));
        {expression, Exp} ->
            environment:assign_var(Environment, {identifier, "IT"}, evaluate(Environment, Exp));
        {switch, Cases} ->
            case catch try_cases(Environment, Cases) of
                {gtfo, E} when is_record(E, env) -> environment:strip_scope(E);
                E when is_record(E, env) -> E
            end;
        {condition, {OnWin, OnFail, Mebbes}} ->
            CEnv = case vars:cast(environment:get_var(Environment, "IT"), {type, troof}) of
                {troof, win} ->
                    catch engine(environment:new_scope(Environment), OnWin, false);
                {troof, fail} ->
                    case try_mebbes(Environment, Mebbes) of
                        {ok, Res} -> Res;
                        not_found -> catch engine(environment:new_scope(Environment), OnFail, false)
                    end
            end,
            case CEnv of
                {gtfo, E} -> throw({gtfo, environment:strip_scope(E)});
                E -> E
            end;
        {loop, LoopedInstrs} ->
            case catch engine(environment:new_scope(Environment), LoopedInstrs, true) of
                {gtfo, E} -> environment:strip_scope(E);
                E -> E
            end;
        {gtfo} ->
            throw({gtfo, Environment})
    end,
    case Looped of
        true -> engine(NewEnv, Program ++ [Instruction], true);
        false -> engine(NewEnv, Program, false)
    end.

run(Program) ->
    run(Program, undefined).
run(Program, Collector) when is_list(Program) ->
    EndEnv=engine(environment:new(Collector), Program, false),
    case Collector of
        Pid when is_pid(Pid) -> Pid ! eof, eof;
        undefined -> eof
    end,
    EndEnv.

%%%%%%%%%

test_set() -> [
    {"evaluate val", fun({Env, Exp}) -> evaluate(Env, Exp) end,
        {environment:new(), {yarn, "abc"}},
        {yarn, "abc"}},
    {"evaluate binop_sum", fun({Env, Exp}) -> evaluate(Env, Exp) end,
        {environment:new(), {binary_math_op, kw_sum, {numbr, 1}, {numbr, 2}}},
        {numbr, 3}},
    {"evaluate binop convert", fun({Env, Exp}) -> evaluate(Env, Exp) end,
        {environment:new(), {binary_math_op, kw_sum, {numbr, 1}, {numbar, 2.5}}},
        {numbar, 3.5}},
    {"evaluate binop variable", fun({Env, Exp}) -> evaluate(Env, Exp) end,
        {environment:create_var(environment:new(), "abc", {numbr, 2}), {binary_math_op, kw_sum, {numbr, 1}, {identifier, "abc"}}},
        {numbr, 3}},
    {"evaluate cast binop", fun({Env, Exp}) -> evaluate(Env, Exp) end,
        {environment:new(), {cast, {binary_math_op, kw_sum, {numbr, 1}, {numbr, 2}}, {type, yarn}}},
        {yarn, "3"}},
    {"evaluate binop_biggr", fun({Env, Exp}) -> evaluate(Env, Exp) end,
        {environment:new(), {binary_math_op, kw_biggr, {numbr, 2}, {numbar, 1.0}}},
        {numbr, 2}},
    {"evaluate binop_smallr", fun({Env, Exp}) -> evaluate(Env, Exp) end,
        {environment:new(), {binary_math_op, kw_smallr, {numbr, 2}, {numbar, 1.0}}},
        {numbar, 1.0}},
    {"evaluate cast_troof", fun({Env, Exp}) -> evaluate(Env, Exp) end,
        {environment:new(), {cast, {numbr, 1}, {type, troof}}},
        {troof, win}},
    {"cast_inplace", fun({Vars, Var, Type}) -> cast_inplace(Vars, Var, Type) end,
        {[[{"abc", {troof, win}}]], "abc", {type, yarn}},
        [[{"abc", {yarn, "WIN"}}]]},
    {"evaluate binop_won", fun({Env, Exp}) -> evaluate(Env, Exp) end,
       {environment:new(), {binary_logic_op, kw_won, {troof,win}, {numbr,1}}},
       {troof, fail}},
    {"evaluate binop_compare1", fun({Env, Exp}) -> evaluate(Env, Exp) end,
       {environment:new(), {binary_compare_op, kw_both, {troof,win}, {numbr,1}}},
       {troof, fail}},
    {"evaluate binop_compare2", fun({Env, Exp}) -> evaluate(Env, Exp) end,
       {environment:new(), {binary_compare_op, kw_both, {numbar,1.0}, {numbr,1}}},
       {troof, win}},
    {"evaluate binop_compare3", fun({Env, Exp}) -> evaluate(Env, Exp) end,
       {environment:new(), {binary_compare_op, kw_both, {yarn,"abc"}, {yarn,"abc"}}},
       {troof, win}},
    {"evaluate binop_compare4", fun({Env, Exp}) -> evaluate(Env, Exp) end,
       {environment:new(), {binary_compare_op, kw_both, {yarn,"abc"}, {yarn,"def"}}},
       {troof, fail}},
    {"evaluate binop_compare5", fun({Env, Exp}) -> evaluate(Env, Exp) end,
       {environment:new(), {binary_compare_op, kw_diffrint, {troof,win}, {numbr,1}}},
       {troof, win}},
    {"evaluate unop", fun({Env, Exp}) -> evaluate(Env, Exp) end,
       {environment:new(), {unary_op, kw_not, {troof,win}}},
       {troof, fail}},
    {"condition, it assign", fun({Environment, Program, Looped}) -> engine(Environment, Program, Looped) end,
       {environment:new(), [
           {expression, {troof,win}},
           {condition, {[{expression, {troof,fail}}],[], []}},
           {expression, {numbr,1}}], false},
       environment:strip_scope(environment:assign_var(environment:new(), {identifier, "IT"}, {numbr,1}))},
    {"condition, it assign scope", fun({Environment, Program, Looped}) -> engine(Environment, Program, Looped) end,
       {environment:new(), [
           {expression, {troof,win}},
           {condition, {[{expression, {troof,fail}}],[], []}}], false},
       environment:strip_scope(environment:assign_var(environment:new(), {identifier, "IT"}, {troof,fail}))},
    {"condition mebbe, it assign", fun({Environment, Program, Looped}) -> engine(Environment, Program, Looped) end,
       {environment:new(), [
           {expression, {troof,fail}},
           {condition, {[],[], [{{troof,win},[{expression, {numbr,1}}]}]}} ], false},
       environment:strip_scope(environment:assign_var(environment:new(), {identifier, "IT"}, {numbr,1}))},
    {"gtfo loop", fun({Environment, Program, Looped}) -> engine(Environment, Program, Looped) end,
       {environment:new(), [
           {loop,[{expression, {troof, fail}}, {gtfo}, {expression, {troof, win}}]}], false},
       environment:strip_scope(environment:assign_var(environment:new(), {identifier, "IT"}, {troof,fail}))},
    {"cut_newline1", fun(A) -> cut_newline([], A) end, "abcde\n", "abcde"},
    {"cut_newline2", fun(A) -> cut_newline([], A) end, "abcde", "abcde"},
    {"visible buffer", fun({Environment, Program}) -> NewEnv = engine(Environment, Program, false),
        environment:get_output(NewEnv) end,
       {environment:new(), [{visible, [{troof,win}]}]},
       "WIN"},
    {"switch", fun({Environment, Program}) -> engine(Environment, Program, false) end,
       {environment:new(), [
           {expression, {numbr,1}},
           {switch, [
               {{numbr,2}, [{expression,{yarn,"incorrect"}}]},
               {{numbr,1}, [{expression,{yarn,"correct"}}]}
               ]}
           ]},
       environment:strip_scope(environment:assign_var(environment:new(), {identifier, "IT"}, {yarn,"correct"}))},
    {"switch fall", fun({Environment, Program}) -> engine(Environment, Program, false) end,
       {environment:new(), [
           {expression, {numbr,2}},
           {switch, [
               {{numbr,2}, [{expression,{yarn,"incorrect"}}]},
               {{numbr,1}, [{expression,{yarn,"correct"}}]}
               ]}
           ]},
       environment:strip_scope(environment:assign_var(environment:new(), {identifier, "IT"}, {yarn,"correct"}))},
    {"switch gtfo", fun({Environment, Program}) -> engine(Environment, Program, false) end,
       {environment:new(), [
           {expression, {numbr,2}},
           {switch, [
               {{numbr,2}, [{expression,{yarn,"correct"}}, {gtfo}]},
               {{numbr,1}, [{expression,{yarn,"incorrect"}}]}
               ]}
           ]},
       environment:strip_scope(environment:assign_var(environment:new(), {identifier, "IT"}, {yarn,"correct"}))},
    {"switch catchall", fun({Environment, Program}) -> engine(Environment, Program, false) end,
       {environment:new(), [
           {expression, {numbr,2}},
           {switch, [
               {{numbr,1}, [{expression,{yarn,"incorrect"}}, {gtfo}]},
               {catchall, [{expression,{yarn,"correct"}}]}
               ]}
           ]},
       environment:strip_scope(environment:assign_var(environment:new(), {identifier, "IT"}, {yarn,"correct"}))}
	].

test() -> tester:test(test_set()).
